﻿//////////////////////////////////////////////////////////////////////////////////
//
// Project            : XNAsteroids
// Module             : XNAsteroids
// Description        : Main game class.
// 
// Repository         : $URL$
// Last changed by    : $LastChangedBy$
// Revision           : $LastChangedRevision$
// Last Changed       : $LastChangedDate$
// Author             : $Author$
//
// Id                 : $Id$
//
// Copyright          : (c) 2010 Torsten Bär
//
// Published under the MIT License. See license.txt or http://www.opensource.org/licenses/mit-license.php.
//
//////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using XNAsteroids.GameObjects.SpaceObjects;
using XNAsteroids.GameObjects.SpaceObjects.SpaceObjects;

namespace XNAsteroids
{
   /// <summary>
   /// Main game class.
   /// </summary>
   public class XNAsteroids : Game
   {
      private const int MIN_ASTEROID_RADIUS = 10;
      private const int MAX_ASTEROID_RADIUS = 50;
      private const int INITIAL_ASTEROID_COUNT = 10;
      private const float DEFAULT_LEVEL_STRING_SHOWTIME = 1.5f;
      private const float MIN_EXPLOSION_SCALE = 5;
      private const float MAX_EXPLOSION_SCALE = 30.0f;

      #region Fields

      private BasicEffect basicEffect;
      private BasicEffect explosionBasicEffect;
      private VertexPositionTexture[] explosionModel;

      private Random random = new Random();

      // game objects
      private SpriteFont smallTextFont;
      private SpriteFont largeTextFont;
      private StarShip starShip;
      private ProjectileCollection projectiles;
      private AsteroidCollection asteroids;
      private StarField starField;

      // state variables
      private readonly TimeSpan fireFrequency = TimeSpan.FromMilliseconds(150);
      private readonly TimeSpan keyboardMinHitRate = TimeSpan.FromMilliseconds(100);
      private DateTime lastShoot = DateTime.Now;
      private bool starShipDestroyed;
      private bool animateExplosion;
      private float explosionScale;
      private bool showLevelSwitch;
      private TimeSpan remainingLevelShowTime;
      private int currentLevel = 1;
      private int remainingLives;

      // bounding frustum for world wrap detection
      private BoundingBox boundingBox;
      private int asteroidCount;
      private long score;
      private bool showHints;
      private DateTime lastHit;
      private Vector3 starshipAccellaration;
      private SpriteBatch spriteBatch;
      private Vector2 liveStringPosition;
      private Vector2 scoreStringPosition;

      #endregion

      #region Properties

      /// <summary>
      /// Gets the width of the screen.
      /// </summary>
      private int ScreenWidth
      {
         get { return GraphicsDevice.Viewport.Width; }
      }

      /// <summary>
      /// Gets the height of the screen.
      /// </summary>
      private int ScreenHeight
      {
         get { return GraphicsDevice.Viewport.Height; }
      }

      #endregion

      #region Constructor

      /// <summary>
      /// Initializes a new instance of the <see cref="XNAsteroids"/> class.
      /// </summary>
      public XNAsteroids()
      {
         // necessary to initialize graphics device
         new GraphicsDeviceManager(this);

         Content.RootDirectory = "Content";
         Window.Title = "XNAsteroids";
      }

      #endregion

      #region Methods

      #region overrides

      /// <summary>
      /// Called after the Game and GraphicsDevice are created, but before LoadContent.  Reference page contains code sample.
      /// </summary>
      protected override void Initialize()
      {
         projectiles = new ProjectileCollection(this);
         asteroids = new AsteroidCollection(this);
         // add to components will cause default behaviour for game components
         // e.g. update, draw, etc.
         Components.Add(projectiles);
         Components.Add(asteroids);

         explosionScale = MIN_EXPLOSION_SCALE;
         asteroidCount = INITIAL_ASTEROID_COUNT;
         remainingLevelShowTime = TimeSpan.FromSeconds(DEFAULT_LEVEL_STRING_SHOWTIME);
         showLevelSwitch = true;

         base.Initialize();
      }

      /// <summary>
      /// LoadContent will be called once per game and is the place to load
      /// all of your content.
      /// </summary>
      protected override void LoadContent()
      {
         // initilized font
         smallTextFont = Content.Load<SpriteFont>("HUDFont");
         largeTextFont = Content.Load<SpriteFont>("Level");

         // switch to counter clockwise vertices
         GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;

         // create basic effect
         basicEffect = new BasicEffect(GraphicsDevice, null)
                       {
                          Projection = Matrix.CreateOrthographicOffCenter(0, ScreenWidth, 0, ScreenHeight, 0, -1),
                       };

         InitializeStars();

         InitalizeStarShip();

         InitializeAsteroids();

         // world bounding box for world wrap detection
         Viewport viewport = GraphicsDevice.Viewport;
         boundingBox = new BoundingBox(
            new Vector3(viewport.X, viewport.Y, viewport.MinDepth),
            new Vector3(viewport.Width, viewport.Height, viewport.MaxDepth));

         spriteBatch = new SpriteBatch(GraphicsDevice);

         InitializeExplosionModelAndTexture();

         remainingLives = 5;
      }

      /// <summary>
      /// UnloadContent will be called once per game and is the place to unload
      /// all content.
      /// </summary>
      protected override void UnloadContent()
      {
      }

      /// <summary>
      /// Allows the game to run logic such as updating the world,
      /// checking for collisions, gathering input, and playing audio.
      /// </summary>
      /// <param name="gameTime">Provides a snapshot of timing values.</param>
      protected override void Update(GameTime gameTime)
      {
         UpdateGameState(gameTime);

         //
         // update keyboard state
         //
         KeyboardState keyboardState = Keyboard.GetState();

         // Allows the game to exit
         if (keyboardState.IsKeyDown(Keys.Escape))
            Exit();

         // reset game on end
         if (keyboardState.IsKeyDown(Keys.R) && lastHit.Add(keyboardMinHitRate) < DateTime.Now && remainingLives == 0)
            Reset();

         // no remaining lives - skip all other updates
         if (remainingLives == 0)
            return;

         // show hints
         if (keyboardState.IsKeyDown(Keys.H) && lastHit.Add(keyboardMinHitRate) < DateTime.Now)
         {
            showHints = !showHints;
            lastHit = DateTime.Now;
         }

         //
         // Update Game objects
         // 

         if (!starShipDestroyed)
            UpdateStarship(gameTime, keyboardState);

         // update projectile positions
         projectiles.Update(gameTime);

         if (!starShipDestroyed)
         {
            // should fire projectile
            if (fireFrequency < DateTime.Now - lastShoot && Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Space))
            {
               FireProjectile();
               lastShoot = DateTime.Now;
            }
         }

         DetectCollisions(gameTime);

         // Wrap the world
         WrapWorld();

         UpdateExplosion(gameTime);

         base.Update(gameTime);
      }

      /// <summary>
      /// This is called when the game should draw itself.
      /// </summary>
      /// <param name="gameTime">Provides a snapshot of timing values.</param>
      protected override void Draw(GameTime gameTime)
      {
         GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);

         DrawText(gameTime);

         if (remainingLives > 0)
            DrawGameObjects(gameTime);
      }

      #endregion

      #region Helper

      #region DrawMethods

      /// <summary>
      /// Draws the game objects.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      private void DrawGameObjects(GameTime gameTime)
      {
         if (!showLevelSwitch)
         {
            asteroids.Draw(gameTime);
            projectiles.Draw(gameTime);

            if (showHints)
               DrawVertices();
         }

         if (currentLevel > 1 || !showLevelSwitch)
         {
            if (!starShipDestroyed)
               starShip.Draw(gameTime);
            else if (animateExplosion)
               DrawExplosion();
         }
      }

      /// <summary>
      /// Draws the text.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      private void DrawText(GameTime gameTime)
      {
         spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred, SaveStateMode.SaveState);

         if (remainingLives > 0)
         {
            if (showLevelSwitch)
               DrawLevelString();

            if (currentLevel > 1 || !showLevelSwitch)
            {
               DrawLiveString();
               DrawScoreString();
            }
         }
         else
         {
            DrawFinalScoreString();
         }

         spriteBatch.End();

         starField.Draw(gameTime);
      }

      /// <summary>
      /// Draws the live string.
      /// </summary>
      private void DrawLiveString()
      {
         string liveString = "";
         for (int idx = 0; idx < remainingLives; idx++)
            liveString += "♥";

         liveStringPosition = new Vector2(10, 10);
         spriteBatch.DrawString(smallTextFont, "Lives: " + liveString, liveStringPosition, Color.White);
      }

      /// <summary>
      /// Draws the level string.
      /// </summary>
      private void DrawLevelString()
      {
         string text = String.Format("Level {0}", currentLevel);

         spriteBatch.DrawString(largeTextFont, text, CenterStringOnScreen(text, largeTextFont), Color.White);
      }

      /// <summary>
      /// Draws the score string.
      /// </summary>
      private void DrawScoreString()
      {
         scoreStringPosition = new Vector2(10, 25);
         spriteBatch.DrawString(smallTextFont, String.Format("Score: {0}", score), scoreStringPosition, Color.White);
      }

      /// <summary>
      /// Draws the final score string.
      /// </summary>
      private void DrawFinalScoreString()
      {
         string text = String.Format("Score {0}", score);
         spriteBatch.DrawString(largeTextFont, text, CenterStringOnScreen(text, largeTextFont), Color.White);
      }

      /// <summary>
      /// Draws the explosion.
      /// </summary>
      private void DrawExplosion()
      {
         explosionBasicEffect.Begin(SaveStateMode.SaveState);

         explosionBasicEffect.World = Matrix.CreateScale(explosionScale) *
                                      Matrix.CreateTranslation(starShip.Position);

         GraphicsDevice.VertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionTexture.VertexElements);
         GraphicsDevice.RenderState.AlphaBlendEnable = true;
         GraphicsDevice.RenderState.SourceBlend = Blend.One;
         GraphicsDevice.RenderState.DestinationBlend = Blend.One;

         foreach (EffectPass effectPass in explosionBasicEffect.CurrentTechnique.Passes)
         {
            effectPass.Begin();

            GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleFan, explosionModel, 0, explosionModel.Length - 2);

            effectPass.End();
         }
         GraphicsDevice.RenderState.AlphaBlendEnable = false;
         explosionBasicEffect.End();
      }

      /// <summary>
      /// Draws the vertices.
      /// </summary>
      private void DrawVertices()
      {
         BasicEffect effect = (BasicEffect)basicEffect.Clone(GraphicsDevice);
         effect.VertexColorEnabled = true;
         GraphicsDevice.VertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);

         effect.Begin(SaveStateMode.SaveState);

         foreach (EffectPass effectPass in effect.CurrentTechnique.Passes)
         {
            effectPass.Begin();

            asteroids.ForEach(a =>
            {
               DrawBoundingSphere(a.BoundingSphere);
               DrawDirectionAndSpeed(a);
               DrawRotationSpeed(a);
            });

            projectiles.ForEach(p =>
            {
               DrawBoundingSphere(p.BoundingSphere);
               DrawDirectionAndSpeed(p);
            });

            DrawBoundingSphere(starShip.BoundingSphere);
            DrawDirectionAndSpeed(starShip);
            DrawAccelaration();

            effectPass.End();
         }
      }

      /// <summary>
      /// Draws the bounding sphere.
      /// </summary>
      /// <param name="boundingSphere">The bounding sphere.</param>
      private void DrawBoundingSphere(BoundingSphere boundingSphere)
      {
         GraphicsDevice.DrawUserPrimitives(PrimitiveType.PointList, GetVertices(boundingSphere).ToArray(), 0, GetVertices(boundingSphere).Count);
      }

      /// <summary>
      /// Draws the direction and speed.
      /// </summary>
      /// <param name="spaceObject">The space object.</param>
      private void DrawDirectionAndSpeed(SpaceObject spaceObject)
      {
         VertexPositionColor[] vertices = new[]
                                          {
                                             new VertexPositionColor(spaceObject.Position, Color.Yellow),
                                             new VertexPositionColor(spaceObject.Position + spaceObject.Speed, Color.Yellow)
                                          };

         GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0, vertices.Length / 2);
      }

      /// <summary>
      /// Draws the rotation speed.
      /// </summary>
      /// <param name="asteroid">The asteroid.</param>
      private void DrawRotationSpeed(Asteroid asteroid)
      {
         Vector3 radialPosition = Vector3.Normalize(asteroid.Position) * asteroid.Radius;

         float cosRotateZ = (float)Math.Cos(-asteroid.Angle);
         float sinRotateZ = (float)Math.Sin(-asteroid.Angle);

         // compute radial position based on rotation
         radialPosition = new Vector3(
            radialPosition.X * cosRotateZ + radialPosition.Y * sinRotateZ,
            radialPosition.X * -sinRotateZ + radialPosition.Y * cosRotateZ,
            asteroid.Position.Z);

         Vector3 speedIndicator;

         if (asteroid.RotationDirection > 0)
         {
            speedIndicator = asteroid.Position + new Vector3(radialPosition.X - radialPosition.Y,
                                                             radialPosition.Y + radialPosition.X,
                                                             asteroid.Position.Z);
         }
         else
         {
            speedIndicator = asteroid.Position + new Vector3(radialPosition.X + radialPosition.Y,
                                                             radialPosition.Y - radialPosition.X,
                                                             asteroid.Position.Z);
         }

         radialPosition = asteroid.Position + radialPosition;

         VertexPositionColor[] vertices = new[]
                                          {
                                             new VertexPositionColor(asteroid.Position, Color.Red),
                                             new VertexPositionColor(radialPosition, Color.Red),
                                             new VertexPositionColor(radialPosition, Color.Red),
                                             new VertexPositionColor(speedIndicator, Color.Red),
                                          };

         GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0, vertices.Length / 2);
      }

      /// <summary>
      /// Draws the accelaration.
      /// </summary>
      private void DrawAccelaration()
      {
         VertexPositionColor[] vertices = new[]
                                          {
                                             new VertexPositionColor(starShip.Position, Color.Blue),
                                             new VertexPositionColor(starShip.Position + starshipAccellaration * 25, Color.Yellow),
                                          };

         GraphicsDevice.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0, 1);
      }

      #endregion

      #region Initializer Methods

      /// <summary>
      /// Initalizes the star ship.
      /// </summary>
      private void InitalizeStarShip()
      {
         Components.Remove(starShip);
         // initialize starship
         starShip = new StarShip(this, new Vector3(ScreenWidth * .5f,
                                                   ScreenHeight * .5f, 0), (BasicEffect)basicEffect.Clone(GraphicsDevice))
         {
            Heading = Vector3.UnitY,
            Speed = Vector3.Zero
         };
         starShip.Initialize();
         Components.Add(starShip);
      }

      /// <summary>
      /// Initializes the stars.
      /// </summary>
      private void InitializeStars()
      {
         Components.Remove(starField);
         // initialize background
         starField = new StarField(this, (BasicEffect)basicEffect.Clone(GraphicsDevice));
         starField.Initialize();
         Components.Add(starField);
      }

      /// <summary>
      /// Initializes the explosion model and texture.
      /// </summary>
      private void InitializeExplosionModelAndTexture()
      {
         // create model
         explosionModel = new[]
                          {
                             new VertexPositionTexture(new Vector3(-0.5f, 0.5f, 0), new Vector2(0, 0)),
                             new VertexPositionTexture(new Vector3(-0.5f, -0.5f, 0), new Vector2(0, 1)),
                             new VertexPositionTexture(new Vector3(0.5f, -0.5f, 0), new Vector2(1, 1)),
                             new VertexPositionTexture(new Vector3(0.5f, 0.5f, 0), new Vector2(1, 0)),
                          };

         // initialize effekt
         explosionBasicEffect = (BasicEffect)basicEffect.Clone(GraphicsDevice);
         explosionBasicEffect.Texture = Content.Load<Texture2D>("explode");
         explosionBasicEffect.TextureEnabled = true;
      }

      /// <summary>
      /// Initializes the asteroids.
      /// </summary>
      private void InitializeAsteroids()
      {
         asteroids.Clear();

         for (int idx = 0; asteroids.Count < asteroidCount; idx++)
         {
            // random radius
            int radius = random.Next(MIN_ASTEROID_RADIUS, MAX_ASTEROID_RADIUS);

            // random position
            Vector3 position = new Vector3(random.Next(0, ScreenWidth),
                                           random.Next(0, ScreenHeight), 0);

            // bounding sphere of supposed new asteroid
            BoundingSphere asteroidSphere = new BoundingSphere(position, radius);

            // prevent asteroid @ starship position
            if (starShip.BoundingSphere.Intersects(asteroidSphere))
               continue;

            // prevent asteroids overlay
            if (asteroids.Any(a => new BoundingSphere(a.Position, a.Radius).Intersects(asteroidSphere)))
               continue;

            // create and initialize new asteroid
            Asteroid asteroid = new Asteroid(this, position, radius, (BasicEffect)basicEffect.Clone(GraphicsDevice))
                                {
                                   Angle = (float)(random.NextDouble() * 360.0f) * (random.Next(0, 1) % 2 == 0 ? -1 : 1),
                                   Speed = new Vector3(random.Next(-10, 10), random.Next(-10, 10), starShip.Position.Z)
                                };
            asteroid.Initialize();
            asteroids.Add(asteroid);
         }
      }

      /// <summary>
      /// Resets the game state.
      /// </summary>
      private void Reset()
      {
         starShip.Position = new Vector3(ScreenWidth * .5f, ScreenHeight * .5f, 0);
         starShip.Heading = Vector3.UnitY;
         starShip.Speed = Vector3.Zero;

         asteroidCount = INITIAL_ASTEROID_COUNT;
         score = 0;
         remainingLives = 5;
         currentLevel = 1;
         remainingLevelShowTime = TimeSpan.FromSeconds(DEFAULT_LEVEL_STRING_SHOWTIME);

         InitializeAsteroids();

         showLevelSwitch = true;
         animateExplosion = false;
      }

      /// <summary>
      /// Resets the starship position and speed.
      /// </summary>
      private void ResetStarshipPositionAndSpeed()
      {
         starShip.Position = new Vector3(ScreenWidth * .5f, ScreenHeight * .5f, 0);
         starShip.Speed = Vector3.Zero;
      }

      #endregion

      #region Update Methods

      /// <summary>
      /// Wraps the world.
      /// </summary>
      private void WrapWorld()
      {
         starShip.WorldWrap(boundingBox);
         projectiles.ForEach(projectile => projectile.WorldWrap(boundingBox));
         asteroids.ForEach(asteroid => asteroid.WorldWrap(boundingBox));
      }

      /// <summary>
      /// Updates the explosion.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      private void UpdateExplosion(GameTime gameTime)
      {
         if (animateExplosion)
         {
            explosionScale += MAX_EXPLOSION_SCALE * (float)gameTime.ElapsedGameTime.TotalSeconds;
            if (explosionScale > MAX_EXPLOSION_SCALE)
               explosionScale = MAX_EXPLOSION_SCALE;
            animateExplosion = explosionScale <= MAX_EXPLOSION_SCALE;
         }
      }

      /// <summary>
      /// Updates the state of the game.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      private void UpdateGameState(GameTime gameTime)
      {
         if (!animateExplosion && starShipDestroyed)
         {
            ResetStarshipPositionAndSpeed();
            starShipDestroyed = asteroids.Any(a => a.BoundingSphere.Intersects(starShip.BoundingSphere));
         }

         if (asteroids.Count == 0 && !showLevelSwitch)
         {
            currentLevel++;

            remainingLevelShowTime = TimeSpan.FromSeconds(DEFAULT_LEVEL_STRING_SHOWTIME);
            showLevelSwitch = true;
         }
         else if (showLevelSwitch)
         {
            remainingLevelShowTime -= gameTime.ElapsedGameTime;
            showLevelSwitch = remainingLevelShowTime.TotalSeconds > 0;

            if (!showLevelSwitch && asteroids.Count == 0)
            {
               asteroidCount += Convert.ToInt32(asteroidCount * 1.33f);
               InitializeAsteroids();
            }
         }
      }

      /// <summary>
      /// Updates starship position, rotation and speed.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      /// <param name="keyboardState">State of the keyboard.</param>
      private void UpdateStarship(GameTime gameTime, KeyboardState keyboardState)
      {
         float power = 0;

         if (keyboardState.IsKeyDown(Keys.Up))
            power = Constants.JETPOWER * (float)gameTime.ElapsedGameTime.TotalSeconds;

         bool isKeyDownLeft = keyboardState.IsKeyDown(Keys.Left);

         if (isKeyDownLeft || keyboardState.IsKeyDown(Keys.Right))
         {
            float rotateZ = (isKeyDownLeft ? -5 : 5) * (float)gameTime.ElapsedGameTime.TotalSeconds;

            // compute new heading
            float sinRotateZ = (float)Math.Sin(rotateZ);
            float cosRotateZ = (float)Math.Cos(rotateZ);

            starShip.Heading = new Vector3(
               starShip.Heading.X * cosRotateZ + starShip.Heading.Y * sinRotateZ,
               starShip.Heading.X * -sinRotateZ + starShip.Heading.Y * cosRotateZ,
               starShip.Heading.Z);
         }

         starshipAccellaration = Vector3.Zero;
         if (power > 0.0)
         {
            // compute speed
            starshipAccellaration = starShip.Heading * power;
            Vector3 newStarshipSpeed = starShip.Speed + starshipAccellaration;
            if (newStarshipSpeed.LengthSquared() > Constants.MAX_STARSHIP_SPEED_SQR)
            {
               newStarshipSpeed.Normalize();
               newStarshipSpeed = newStarshipSpeed * Constants.MAX_STARSHIP_SPEED;
            }
            starShip.Speed = newStarshipSpeed;
         }
      }

      /// <summary>
      /// Fires a projectile.
      /// </summary>
      private void FireProjectile()
      {
         projectiles.Add(new Projectile(this, starShip.Position, (BasicEffect)basicEffect.Clone(GraphicsDevice))
                         {
                            Speed = starShip.Heading * (starShip.Speed.Length() + Constants.PROJECTILE_SHOOTOUTSPEED)
                         });
      }
      
      #endregion

      /// <summary>
      /// Centers the string on screen.
      /// </summary>
      /// <param name="text">The text.</param>
      /// <param name="font">The font.</param>
      /// <returns>The position.</returns>
      private Vector2 CenterStringOnScreen(string text, SpriteFont font)
      {
         Vector2 measuredString = font.MeasureString(text) * .5f;

         return new Vector2(ScreenWidth * .5f - measuredString.X, ScreenHeight * .5f - measuredString.Y);
      }

      /// <summary>
      /// Detects the collisions.
      /// </summary>
      /// <param name="gameTime">The game time.</param>
      private void DetectCollisions(GameTime gameTime)
      {
         // starship hits asteroids?
         if (!starShipDestroyed && !animateExplosion && asteroids.Any(a => a.HitTest(starShip)))
         {
            starShipDestroyed = true;
            animateExplosion = true;
            explosionScale = MIN_EXPLOSION_SCALE;
            remainingLives--;
         }

         // detects collisions
         var objectCollisions = projectiles
            .Select(projectile => new
                         {
                            Projectile = projectile,
                            Asteroids = asteroids.Where(asteroid => asteroid.HitTest(projectile)).ToList()
                         })
            .Where(arg => arg.Asteroids.Count > 0);

         // create new asteroids
         objectCollisions.ForEach(arg => arg.Asteroids
                                            .ForEach(asteroid => SplitAsteroid(asteroid, arg.Projectile, gameTime)
                                                                    .ForEach(newAsteroid =>
                                                                             {
                                                                                asteroids.Add(newAsteroid);
                                                                                newAsteroid.Initialize();
                                                                             })));
         // remove old asteroids and projectiles
         objectCollisions
            .ToList() // prevents InvalidOperationException
            .ForEach(p =>
                     {
                        projectiles.Remove(p.Projectile);
                        p.Asteroids.ForEach(a => asteroids.Remove(a));
                     });
      }

      /// <summary>
      /// Splits the asteroid.
      /// </summary>
      /// <param name="asteroid">The asteroid.</param>
      /// <param name="projectile">The projectile.</param>
      /// <param name="gameTime">The game time.</param>
      /// <returns>The resulting asteroids.</returns>
      private IEnumerable<Asteroid> SplitAsteroid(Asteroid asteroid, Projectile projectile, GameTime gameTime)
      {
         score += Convert.ToInt32(MAX_ASTEROID_RADIUS - asteroid.Radius + MIN_ASTEROID_RADIUS);

         // computes projectile direction
         Ray projectileMovement = new Ray(projectile.Position, Vector3.Normalize(projectile.Speed));

         Vector3 hitVertex = Vector3.Normalize(asteroid.Position - projectile.Position);

         // compute new asteroid speed
         float speed = asteroid.Speed.Length() * (float)gameTime.ElapsedGameTime.TotalSeconds;

         // initialize asteroids
         return new[]
                {
                   new Asteroid(this, asteroid.Position + hitVertex * speed, asteroid.Radius * 0.66f, (BasicEffect)basicEffect.Clone(GraphicsDevice))
                   {
                      Speed = asteroid.Speed.Length() * -hitVertex,
                   },
                   new Asteroid(this, asteroid.Position + projectileMovement.Direction * speed, asteroid.Radius * 0.33f, (BasicEffect)basicEffect.Clone(GraphicsDevice))
                   {
                      Speed = asteroid.Speed.Length() * projectileMovement.Direction,
                   },
                };
      }

      /// <summary>
      /// Gets the vertices.
      /// </summary>
      /// <param name="boundingSphere">The bounding sphere.</param>
      /// <returns>Vertices of the BoundingSphere.</returns>
      private static List<VertexPositionColor> GetVertices(BoundingSphere boundingSphere)
      {
         List<VertexPositionColor> vertices = new List<VertexPositionColor>();
         for (int angle = 0; angle < 360; angle += 20)
         {
            Vector3 vertextPosition = new Vector3((float)Math.Cos(MathHelper.ToRadians(angle)) * boundingSphere.Radius,
                                                  (float)Math.Sin(MathHelper.ToRadians(angle)) * boundingSphere.Radius, 0);

            vertices.Add(new VertexPositionColor(vertextPosition + boundingSphere.Center, Color.Red));
         }
         return vertices;
      }

      #endregion

      #endregion
   }
}
